在使用 Web Worker 和 SharedArrayBuffer 时,很多开发者会遇到类似现象:

        
worker.postMessage({
  buffer: sharedBuffer
});

console.log('主线程后续代码');
        
      

即使 postMessage() 写在前面,控制台中也可能先看到:

        
主线程后续代码
Worker 收到消息
        
      

这并不是 SharedArrayBuffer 失效,也不是浏览器执行顺序混乱,而是由浏览器的事件循环、任务队列和线程调度机制共同决定的。

本文将系统介绍:

  • postMessage() 到底做了什么

  • 为什么消息处理函数不会立即执行

  • SharedArrayBuffer 为什么不会自动阻塞

  • 如何正确使用 Atomics 实现同步

  • postMessage 和 SharedArrayBuffer 应该如何配合使用


一、先看一个简单示例

主线程

        
const worker = new Worker('./worker.js');

const sharedBuffer = new SharedArrayBuffer(4);
const sharedArray = new Int32Array(sharedBuffer);

sharedArray[0] = 100;

worker.postMessage({
  buffer: sharedBuffer
});

console.log('主线程:postMessage 之后执行');
        
      

Worker 线程

        
self.onmessage = function (event) {
  const { buffer } = event.data;
  const array = new Int32Array(buffer);

  console.log('Worker:收到消息');
  console.log('共享数据:', array[0]);
};
        
      

通常输出顺序是:

        
主线程:postMessage 之后执行
Worker:收到消息
        
      

很多人会认为:

        
worker.postMessage(...)
        
      

应该立即触发 Worker 中的:

        
self.onmessage = ...
        
      

但实际上并不是这样。


二、postMessage() 不是函数调用,而是发送异步消息

调用:

        
worker.postMessage(data);
        
      

并不会直接调用 Worker 中的 onmessage 函数。

它的主要作用是:

  1. 将数据发送给 Worker;

  2. 浏览器对消息进行结构化处理;

  3. 将消息放入 Worker 的事件队列;

  4. 等待 Worker 线程调度并处理这条消息。

可以简单理解为:

        
主线程:
    postMessage()
        ↓
    消息进入 Worker 队列
        ↓
    Worker 未来某个时间处理消息
        
      

因此,postMessage() 本身是一个异步通信接口。

它只负责“提交消息”,而不是“等待消息处理完成”。


三、为什么 postMessage() 后面的代码会先执行?

看下面的代码:

        
worker.postMessage('hello');

console.log('A');
        
      

实际过程大致如下:

        
1. 主线程调用 worker.postMessage()
2. 浏览器把消息放入 Worker 的任务队列
3. postMessage() 立即返回
4. 主线程继续执行 console.log('A')
5. 主线程当前任务结束
6. Worker 被调度执行消息处理函数
        
      

所以输出通常是:

        
A
Worker 收到消息
        
      

关键原因是:

postMessage() 只负责发送消息,不负责等待接收方执行完毕。

这和下面的同步函数调用完全不同:

        
function run() {
  console.log('B');
}

run();
console.log('A');
        
      

同步函数调用时,run() 会立即执行完成后,才执行下一行。

而 postMessage() 更接近:

        
queue.push(message);
return;
        
      

接收方什么时候处理消息,由浏览器的事件循环和线程调度决定。


四、SharedArrayBuffer 并不会改变异步执行模型

SharedArrayBuffer 的作用是:

允许主线程和 Worker 共同访问同一块内存。

例如:

        
const sharedBuffer = new SharedArrayBuffer(1024);
const array = new Int32Array(sharedBuffer);
        
      

将它传递给 Worker:

        
worker.postMessage(sharedBuffer);
        
      

Worker 可以创建一个指向同一块内存的视图:

        
self.onmessage = event => {
  const array = new Int32Array(event.data);
  console.log(array[0]);
};
        
      

这里的“共享”指的是:

  • 主线程和 Worker 看到的是同一块底层内存;

  • 一方修改数据,另一方可以看到修改结果;

  • 不需要复制整个缓冲区。

但是,SharedArrayBuffer 并不意味着:

  • 主线程会等待 Worker;

  • Worker 会立即执行;

  • postMessage() 会变成同步调用;

  • 对共享数据的读写天然安全;

  • 不需要使用 Atomics。

也就是说:

SharedArrayBuffer 解决的是“共享内存”问题,而不是“执行顺序”问题。


五、共享内存和线程同步是两件不同的事情

假设主线程执行:

        
const sharedBuffer = new SharedArrayBuffer(4);
const array = new Int32Array(sharedBuffer);

array[0] = 0;

worker.postMessage(sharedBuffer);

array[0] = 1;
        
      

Worker 中:

        
self.onmessage = event => {
  const array = new Int32Array(event.data);

  console.log(array[0]);
};
        
      

Worker 可能输出:

        
1
        
      

因为 Worker 真正执行 onmessage 时,主线程很可能已经执行完:

        
array[0] = 1;
        
      

这并不是 Worker 读取错误,而是因为:

  1. postMessage() 只是发送通知;

  2. 主线程继续向下执行;

  3. Worker 稍后才处理消息;

  4. Worker 读取的是共享内存当前的值。

如果你希望 Worker 读取的是某个确定时刻的数据,就不能只依赖 postMessage() 的调用位置,而应该设计明确的同步协议。


六、postMessage 能保证什么顺序?

对于同一个消息发送方和同一个接收方,消息通常会按照发送顺序进行处理:

        
worker.postMessage('message 1');
worker.postMessage('message 2');
worker.postMessage('message 3');
        
      

Worker 一般会按照以下顺序收到:

        
message 1
message 2
message 3
        
      

但是,这种顺序保证只表示:

消息进入接收方后的处理顺序是有序的。

它不表示:

  • 主线程会等待 Worker 执行;

  • Worker 会在主线程下一行代码之前执行;

  • 共享内存中的读写会自动形成同步关系;

  • 多个线程之间的普通读写不会发生竞争。

例如:

        
worker.postMessage('start');

sharedArray[0] = 100;

console.log('主线程完成');
        
      

不能简单理解为:

        
Worker 一定先看到 postMessage 时的值
        
      

因为消息到达和共享内存后续读写之间,需要通过明确的同步机制建立关系。


七、普通读写可能存在数据竞争

下面是一个典型问题:

主线程

        
sharedArray[0] = 0;

worker.postMessage(sharedBuffer);

sharedArray[0] = 1;
        
      

Worker

        
self.onmessage = event => {
  const array = new Int32Array(event.data);

  if (array[0] === 0) {
    console.log('读取到 0');
  } else {
    console.log('读取到 1');
  }
};
        
      

Worker 读取到什么值,取决于两个线程的实际调度时间。

这就是并发程序中的竞态条件。

如果多个线程同时修改共享数据,还可能出现:

        
sharedArray[0]++;
        
      

这实际上不是一个不可分割的操作,而是:

        
读取
加一
写回
        
      

如果两个线程同时执行,就可能发生数据丢失。

例如:

        
线程 A 读取 0
线程 B 读取 0
线程 A 写入 1
线程 B 写入 1
        
      

最终结果是 1,而不是预期的 2。


八、使用 Atomics 保证原子操作

对于共享的整数类型数据,可以使用 Atomics:

        
Atomics.add(sharedArray, 0, 1);
        
      

它可以保证加法操作是原子的,不会被其他线程拆开。

常用方法包括:

        
Atomics.load(array, index);
Atomics.store(array, index, value);
Atomics.add(array, index, value);
Atomics.sub(array, index, value);
Atomics.exchange(array, index, value);
Atomics.compareExchange(array, index, expected, replacement);
        
      

例如:

        
Atomics.store(sharedArray, 0, 100);

const value = Atomics.load(sharedArray, 0);
console.log(value);
        
      

相比普通读写:

        
sharedArray[0] = 100;
const value = sharedArray[0];
        
      

Atomics 能够提供更明确的并发访问语义。


九、使用 Atomics.wait() 等待线程通知

如果需要让 Worker 等待某个条件,可以使用:

        
Atomics.wait()
        
      

例如,主线程先准备数据,然后通知 Worker。

主线程

        
const buffer = new SharedArrayBuffer(8);
const state = new Int32Array(buffer);

const worker = new Worker('./worker.js');

worker.postMessage(buffer);

setTimeout(() => {
  Atomics.store(state, 0, 123);

  // 通知等待中的 Worker
  Atomics.notify(state, 0, 1);
}, 1000);
        
      

Worker

        
self.onmessage = event => {
  const state = new Int32Array(event.data);

  console.log('Worker 开始等待');

  Atomics.wait(state, 0, 0);

  const value = Atomics.load(state, 0);

  console.log('Worker 被唤醒,读取到:', value);
};
        
      

执行过程是:

        
1. Worker 收到消息
2. Worker 调用 Atomics.wait()
3. Worker 进入等待状态
4. 主线程写入共享数据
5. 主线程调用 Atomics.notify()
6. Worker 被唤醒
7. Worker 读取最新数据
        
      

这里才建立了明确的线程同步关系。


注意:主线程不能使用阻塞式 Atomics.wait()

浏览器主线程调用:

        
Atomics.wait(...)
        
      

通常会抛出异常,因为这会阻塞页面,导致:

  • 页面无法渲染;

  • 用户操作无响应;

  • 浏览器标签页卡死。

因此:

  • Worker 中可以使用 Atomics.wait();

  • 主线程不要使用阻塞式等待;

  • 主线程可以考虑使用 Atomics.waitAsync(),或者通过 postMessage、Promise 等异步方式通知。


十、推荐模式:postMessage 负责传递,SharedArrayBuffer 负责共享,Atomics 负责同步

一个更合理的架构是:

        
postMessage:
    传递 SharedArrayBuffer、任务参数、初始化信息

SharedArrayBuffer:
    存储共享状态和数据

Atomics:
    保证状态修改的原子性
    实现等待和唤醒
        
      

例如设计一个状态值:

        
const STATUS_IDLE = 0;
const STATUS_READY = 1;
const STATUS_DONE = 2;
        
      

主线程

        
const buffer = new SharedArrayBuffer(16);
const shared = new Int32Array(buffer);

const worker = new Worker('./worker.js');

worker.postMessage(buffer);

// 写入任务数据
Atomics.store(shared, 1, 42);

// 设置为 READY
Atomics.store(shared, 0, STATUS_READY);

// 通知 Worker
Atomics.notify(shared, 0, 1);
        
      

Worker

        
self.onmessage = event => {
  const shared = new Int32Array(event.data);

  // 等待状态变为 READY
  while (Atomics.load(shared, 0) !== STATUS_READY) {
    Atomics.wait(shared, 0, STATUS_IDLE);
  }

  const input = Atomics.load(shared, 1);

  console.log('Worker 收到任务:', input);

  const result = input * 2;
  Atomics.store(shared, 1, result);

  // 设置为 DONE
  Atomics.store(shared, 0, STATUS_DONE);

  // 通知主线程
  self.postMessage('done');
};
        
      

这里要注意:

  • postMessage(buffer) 只负责把共享缓冲区交给 Worker;

  • Atomics.store() 写入状态;

  • Atomics.wait() 让 Worker 等待;

  • Atomics.notify() 唤醒 Worker;

  • postMessage('done') 可以作为完成通知。


十一、如何等待 Worker 执行完成?

如果你只是想知道 Worker 什么时候处理完成,通常不需要直接依赖 SharedArrayBuffer,使用普通消息即可:

主线程

        
worker.postMessage({
  type: 'start',
  buffer: sharedBuffer
});

worker.onmessage = event => {
  if (event.data.type === 'done') {
    console.log('Worker 执行完成');
  }
};

console.log('主线程继续执行');
        
      

Worker

        
self.onmessage = event => {
  const { type, buffer } = event.data;

  if (type === 'start') {
    const array = new Int32Array(buffer);

    // 执行任务
    array[0] = array[0] * 2;

    self.postMessage({
      type: 'done'
    });
  }
};
        
      

这种模式的优点是:

  • 简单;

  • 不阻塞主线程;

  • 易于理解;

  • 适合大多数任务完成通知场景。

只有当你需要高性能、低延迟、频繁共享数据或实现生产者消费者模型时,才需要深入使用 Atomics。


十二、SharedArrayBuffer 还需要满足安全条件

现代浏览器通常要求页面处于跨源隔离环境,才能使用 SharedArrayBuffer。

服务器需要设置:

        
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
        
      

例如:

        
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
        
      

前端可以检查:

        
console.log(crossOriginIsolated);
        
      

如果输出:

        
true
        
      

说明当前页面处于跨源隔离状态。

否则可能出现:

        
ReferenceError: SharedArrayBuffer is not defined
        
      

或者浏览器拒绝创建共享缓冲区。


十三、常见误区

误区一:postMessage 调用后,Worker 会立即执行

错误理解:

        
worker.postMessage(data);
// Worker 此时已经执行完成
        
      

正确理解:

        
worker.postMessage(data);
// 消息已经提交,但 Worker 可能还没有开始执行
        
      

误区二:SharedArrayBuffer 会自动同步

错误理解:

        
主线程写入数据后,Worker 一定按照期望顺序读取
        
      

正确理解:

        
SharedArrayBuffer 只提供共享内存;
同步需要使用 Atomics 或消息协议。
        
      

误区三:看到共享数据就不需要传消息

虽然 Worker 可以访问共享数据,但它仍然需要知道:

  • 什么时候有新数据;

  • 哪一段数据可读;

  • 哪一段空间可写;

  • 任务是否完成。

因此通常仍然需要:

  • postMessage

  • Atomics.wait

  • Atomics.notify

  • 状态标记

  • 环形队列等机制


误区四:普通 array[index]++ 是安全的

在多线程环境中:

        
array[0]++;
        
      

不是原子操作。

应改为:

        
Atomics.add(array, 0, 1);
        
      

十四、总结

在浏览器中使用 SharedArrayBuffer 时,postMessage() 后面的代码先执行,是正常现象,主要原因如下:

  1. postMessage() 是异步消息通信,不会立即执行接收方代码;

  2. 消息会先进入 Worker 的事件队列;

  3. 主线程会继续执行当前任务中的后续代码;

  4. Worker 何时执行取决于浏览器调度和线程运行状态;

  5. SharedArrayBuffer 只负责共享内存,不负责线程同步;

  6. 需要确定执行顺序时,应使用 Atomics.load()、Atomics.store()、Atomics.wait() 和 Atomics.notify();

  7. 如果只是等待 Worker 完成,直接使用 Worker 的 postMessage 回传结果通常更简单。

可以用一句话概括:

postMessage 负责“通知”,SharedArrayBuffer 负责“共享数据”,Atomics 负责“同步执行顺序”。

理解这三者的职责之后,就不会再把“共享内存”和“同步执行”混为一谈了。